// Package npc-ddns-go @Author ChenLei 2022/7/28 14:44:00
package main

import (
	"bytes"
	"errors"
	"flag"
	"fmt"
	"github.com/astaxie/beego"
	"github.com/astaxie/beego/logs"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"
)

var (
	commonArgs                               []string
	hour, ddnsTime                           int64
	wanIP                                    string
	serverPort, ddnsPort                     int
	npcProgram, ddnsProgram, registerProgram string
	invalidError                             = errors.New("invalid client,maybe no client program found,You can try adding a client program to the run directory")
)

func init() {
	var configPath string
	flag.StringVar(&configPath, "config", "tool/nat-tool.conf", "")
	flag.Parse()
	if err := beego.LoadAppConfig("ini", configPath); err != nil {
		logs.Error(err.Error())
		os.Exit(-1)
	}
	logs.EnableFuncCallDepth(false)
	if runtime.GOOS == "linux" {
		npcProgram = "/app/tool/npc"
		ddnsProgram = "/app/tool/ddns-go"
		registerProgram = "/app/tool/register"
		logs.Info("system linux")
	}
	if runtime.GOOS == "windows" {
		npcProgram = "/app/tool/npc.exe"
		ddnsProgram = "/app/tool/ddns-go.exe"
		registerProgram = "/app/tool/register.exe"
		logs.Info("system windows")
	}
	serverPort = beego.AppConfig.DefaultInt("server_port", 8191)
	ddnsPort = beego.AppConfig.DefaultInt("ddns::port", 9876)
	commonArgs = append(commonArgs, fmt.Sprintf("-server=%s", beego.AppConfig.String("npc::server_addr")))
	commonArgs = append(commonArgs, fmt.Sprintf("-vkey=%s", beego.AppConfig.String("npc::v_key")))
	commonArgs = append(commonArgs, fmt.Sprintf("-type=%s", beego.AppConfig.String("npc::type")))
	hour = beego.AppConfig.DefaultInt64("npc::time", 2)
	ddnsTime = beego.AppConfig.DefaultInt64("ddns::time", 600)
	commonArgs = append(commonArgs, fmt.Sprintf("-time=%d", hour))
}

//go:generate  go build -o setup.exe setup.go
func main() {
	go client()
	if beego.AppConfig.DefaultBool("npc::auto_register_ip", false) {
		go register(true)
	}
	go listenWanIP()
	go ddns()
	http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
		defer request.Body.Close()
		go register(false)
		writer.WriteHeader(http.StatusNoContent)
	})
	http.HandleFunc("/health", func(writer http.ResponseWriter, request *http.Request) {
		defer request.Body.Close()
		writer.WriteHeader(http.StatusNoContent)
	})
	logs.Info("start http server success")
	logs.Info(fmt.Sprintf("server port %d", serverPort))
	if err := http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil); err != nil {
		logs.Error(err.Error())
	}
}

func register(auto bool) {
	if auto {
		logs.Info("start auto register npc wan ip")
		time.Sleep(10 * time.Second)
	}
	index := 1
	for {
		regCommand := exec.Command(registerProgram, append([]string{"register"}, commonArgs...)...)
		var status bool
		if err := run(regCommand, true); err == nil {
			status = true
		} else if err == invalidError {
			time.Sleep(120 * time.Second)
			continue
		}
		if auto {
			if status {
				logs.Info("auto refresh register hour success")
				time.Sleep(time.Duration(hour*60-40) * time.Minute)
				continue
			}
			kill(regCommand)
			time.Sleep(30 * time.Second)
			continue
		}
		if status {
			logs.Info("refresh register hour success")
			return
		}
		kill(regCommand)
		index++
		if index == 6 {
			logs.Error("too many retries refresh register task,task failed")
			return
		}
		time.Sleep(5 * time.Second)
	}
}

func client() {
	if !beego.AppConfig.DefaultBool("npc::open", false) {
		return
	}
	logs.Info("start listen npc client health status")
	time.Sleep(3 * time.Second)
	var onlineProcess *exec.Cmd
	for {
		npcCommand := exec.Command(npcProgram, commonArgs...)
		var status bool
		for i := 0; i < 6; i++ {
			res, err := http.Get(beego.AppConfig.String("npc::health_url"))
			if err != nil {
				time.Sleep(6 * time.Second)
				continue
			}
			_ = res.Body.Close()
			if res.StatusCode != http.StatusNoContent {
				time.Sleep(6 * time.Second)
				continue
			}
			status = true
			break
		}
		if !status {
			if onlineProcess != nil {
				kill(onlineProcess)
				onlineProcess = nil
			}
			logs.Info("restart npc client")
			var status bool
			if err := run(npcCommand, false); err == nil {
				status = true
			} else if err == invalidError {
				time.Sleep(120 * time.Second)
				continue
			}
			if !status {
				kill(npcCommand)
			} else {
				onlineProcess = npcCommand
				logs.Info("npc client start success")
			}
		}
		time.Sleep(20 * time.Second)
	}
}

func ddns() {
	if !beego.AppConfig.DefaultBool("ddns::open", false) {
		return
	}
	logs.Info("start ddns server")
	time.Sleep(15 * time.Second)
	var onlineProcess *exec.Cmd
	for {
		ddnsCommand := exec.Command(ddnsProgram, "-l", fmt.Sprintf(":%d", ddnsPort), "-f", "600", "-c", "tool/ddns-go.yaml")
		var status bool
		for i := 0; i < 6; i++ {
			res, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/logs", ddnsPort))
			if err != nil {
				time.Sleep(6 * time.Second)
				continue
			}
			_ = res.Body.Close()
			if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusUnauthorized {
				time.Sleep(6 * time.Second)
				continue
			}
			status = true
			break
		}
		if !status {
			if onlineProcess != nil {
				kill(onlineProcess)
				onlineProcess = nil
			}
			logs.Info("restart ddns client")
			var status bool
			if err := run(ddnsCommand, false); err == nil {
				status = true
			} else if err == invalidError {
				time.Sleep(120 * time.Second)
				continue
			}
			if !status {
				kill(ddnsCommand)
			} else {
				onlineProcess = ddnsCommand
				logs.Info("ddns client start success")
			}
		}
		time.Sleep(60 * time.Second)
	}
}

func listenWanIP() {
	if !beego.AppConfig.DefaultBool("npc::wan_change_register", false) {
		return
	}
	logs.Info("start listen wan ip change")
	time.Sleep(15 * time.Second)
	for {
		wanUrls := beego.AppConfig.Strings("npc::wan_url")
		var (
			ip     string
			status bool
		)
		for _, url := range wanUrls {
			res, err := http.Get(url) // 获取外网 IP
			if err != nil {
				continue
			}
			// 程序在使用完 response 后必须关闭 response 的主体。
			body, _ := ioutil.ReadAll(res.Body)
			ip = fmt.Sprintf("%s", strings.Trim(string(body), "\n"))
			_ = res.Body.Close()
			status = true
			break
		}
		if !status {
			time.Sleep(20 * time.Second)
			continue
		}
		if ip != "" && ip != wanIP {
			go register(false)
			wanIP = ip
			logs.Info("wan ip change:", wanIP)
		}
		time.Sleep(time.Duration(ddnsTime) * time.Second)
	}
}

func run(command *exec.Cmd, wait bool) (err error) {
	defer func() {
		if info := recover(); info != nil {
			logs.Error("%s process %s", command.Args[0], invalidError.Error())
			err = invalidError
		}
	}()
	var out bytes.Buffer
	var stderr bytes.Buffer
	command.Stdout = &out
	command.Stderr = &stderr
	logs.Info("cmd", command.Args)
	if wait {
		err = command.Run()
	} else {
		err = command.Start()
	}
	if err != nil {
		logs.Error("%s process %d %s %s", command.Args[0], command.Process.Pid, err.Error(), strings.Trim(stderr.String(), "\n"))
		command.Process.Pid = 0
		return err
	}
	return
}

func kill(command *exec.Cmd) {
	if command.Process.Pid != 0 {
		logs.Error("%s client connect failed, process %d", command.Args[0], command.Process.Pid)
		if err := command.Process.Kill(); err != nil {
			logs.Error("kill %s process %d failed,%s", command.Args[0], command.Process.Pid, err.Error())
			return
		}
		command.Process.Pid = 0
	}
}
